package mbsl.ui;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import mbsl.util.CaselessStringComparator;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;

/**
 * A Combo wrapper, which adds a few convenience methods to the SWT implementation, namely direct adding of Objects.
 */
public class Combo {
	/** Indicates that the default element should be placed at the top. */
	public static final boolean TOP = true;
	/** Indicates that the default element should be placed at the bottom. */
	public static final boolean BOTTOM = false;
	
	/** 
	 * Adds a Combo to the set Composite with the set flags.
	 * @param pParent The parent Composite.
	 * @param pSWTFlags The flags to pass to SWT when creating the underlying Combo widget.
	 */
	public Combo(Composite pParent, int pSWTFlags) {
		mCombo = new org.eclipse.swt.widgets.Combo(pParent, pSWTFlags);
		mContents = new TreeMap<String, Object>(new CaselessStringComparator()); 
		mDefaultElementLocation = null;
		AutoFiller autoFiller = new AutoFiller();
		
		mCombo.addKeyListener(autoFiller);
		mCombo.addModifyListener(autoFiller);
	}
	/**
	 * Adds a Combo to the set Composite with the set flags.
	 * <p>
	 * An element will be added to the top of the Combo with the passed text, or the default text if null is passed.
	 * This element is special in that {@link #getSelected()} will return null if the item is selected.  Behaviour
	 * is unspecified if the empty element shares its label with another item.
	 * @param pParent The parent Composite.
	 * @param pDefaultElementLabel The label of the default element.  Passing null will insert the default text (blank).
	 * @param pSWTFlags The flags to pass to SWT when creating the underlying Combo widget.
	 */
	public Combo(Composite pParent, String pDefaultElementLabel, int pSWTFlags) {
		this(pParent, pDefaultElementLabel, TOP, pSWTFlags);
	}
	/**
	 * Adds a Combo to the set Composite with the set flags.
	 * <p>
	 * An element will be added to the the Combo with the passed text, or the default text if null is passed.
	 * This element is special in that {@link #getSelected()} will return null if the item is selected.  Behaviour
	 * is unspecified if the empty element shares its label with another item.
	 * @param pParent The parent Composite.
	 * @param pDefaultElementLabel The label of the default element.  Passing null will insert the default text (blank).
	 * @param pDefaultElementLocation The location of the default element.  Pass true or {@link #TOP} to have it at the
	 * start of the list, and false or {@link #BOTTOM} to have it at the end of the list.
	 * @param pSWTFlags The flags to pass to SWT when creating the underlying Combo widget.
	 */
	public Combo(Composite pParent, String pDefaultElementLabel, boolean pDefaultElementLocation, int pSWTFlags) {
		this(pParent, pSWTFlags);
		
		mDefaultElementLocation = pDefaultElementLocation;
		
		if (pDefaultElementLabel == null)
			pDefaultElementLabel = DEFAULT_ELEMENT_LABEL;
		
		mContents.put(pDefaultElementLabel, EMPTY_ELEMENT);
		
		mCombo.add(pDefaultElementLabel);
	}
	
	/** Returns the underlying Widget */
	public org.eclipse.swt.widgets.Combo getWidget() {
		return mCombo;
	}
	/** Adds an object to the list */
	public void add(Object pObject) {
		add(pObject, false);
	}
	private void add(Object pObject, boolean pInsertDirectly) {
		mContents.put(pObject.toString(), pObject);
		
		if (!pInsertDirectly) {
			int index = Arrays.binarySearch(mCombo.getItems(), pObject.toString());
			
			if (index >= 0)
				return;
			else
				index = Math.abs(index);
			
			mCombo.add(pObject.toString(), index - 1);
		} else {
			if (mDefaultElementLocation != null && mDefaultElementLocation.booleanValue() == BOTTOM)
				mCombo.add(pObject.toString(), mCombo.getItemCount() - 1);
			else
				mCombo.add(pObject.toString());
		}
	}
	/** Removes an object from the list */
	public void remove(Object pObject) {
		if (!mContents.containsKey(pObject.toString()))
			return;
		
		mCombo.remove(pObject.toString());
		mContents.remove(pObject.toString());
	}
	/** Adds all the objects from the list */
	public void add(List<?> pObjects) {
		for (Object object : pObjects)
			add(object, true);
	}
	/** Adds all the objects from the array */
	public void add(Object... pObjects) {
		for (Object object : pObjects)
			add(object, true);
	}
	public void clear() {
		mCombo.removeAll();
		mContents.clear();
	}
	/** 
	 * Returns the selected object.
	 * <p>
	 * If the name selected is the value of the toString() of an added object, it will return that object.
	 * If the name selected isn't the value of an added object, the name will be returned as a String.
	 * if the name selected is empty, it will return null. 
	 */
	public Object getSelected() {
		if (mCombo.getText().isEmpty() || mContents.get(mCombo.getText()) == EMPTY_ELEMENT)
			// As long as EMPTY_ELEMENT is the empty String, the last predicate isn't strictly necessary, but JIC.
			return null;
		
		Object object = mContents.get(mCombo.getText());
			
		if (object != null)
			return object;
		else
			return mCombo.getText();
	}
	/** 
	 * Sets the selected object.
	 * <p>
	 * Passing null will clear the selection.
	 * <p>
	 * Note that this does not imply {@link #add(Object)}.
	 */
	public void setSelected(Object pObject) {
		if (pObject == null)
			mCombo.setText("");
		else if (pObject instanceof String)
			mCombo.setText((String)pObject);
		else
			mCombo.setText(pObject.toString());
	}

	/** A combined Key- and ModifyListener which enables autocomplete for combo boxes */
	private class AutoFiller implements KeyListener, ModifyListener {
		private String mOriginalText = "";
		/** This flag signals that the next ModifyEvent should be ignored */
		private boolean mSkipNext;
		private int mKeyUsed;
		
		@Override
		public void modifyText(ModifyEvent pE) {
			if (mSkipNext) {
				mSkipNext = false;
				return;
			}
			if (mCombo.getText().isEmpty()) {
				mOriginalText = "";

				return;
			}
			// If the user pressed backspace
			if (mKeyUsed == SWT.BS && mCombo.getText().toLowerCase().equals(mOriginalText.toLowerCase())) {
				mSkipNext = true; // This one is eaten by the setText coming up next
				// This means we should remove the last character in the unmodified text
				mCombo.setText(mOriginalText.substring(0, mOriginalText.length() - 1));
				// and place the marker at the end of it.
				mCombo.setSelection(new Point(mCombo.getText().length(), mCombo.getText().length()));
				// If that leaves the field empty, we return.  Otherwise, we send it down.
				if (mCombo.getText().isEmpty()) {
					mOriginalText = mCombo.getText();

					return;
				}
			}
			
			// Jot down the contents of the box before we start messing with it.
			mOriginalText = mCombo.getText();
			// Find the index of the item named,
			int index = Arrays.binarySearch(mCombo.getItems(), mCombo.getText(), new CaselessStringComparator());
			// or where it would have been if it existed.
			if (index < 0)
				index = Math.abs(index) - 1;
			/* If the index is within bounds, and the item at that index starts with the contents of the
			 * box,
			 */
			if (index < mCombo.getItemCount() && mCombo.getItem(index).toLowerCase().startsWith(mCombo.getText().toLowerCase())) {
				mSkipNext = true;
				// we set the contents to the name of that item,
				mCombo.setText(mCombo.getItem(index));
				// and select the parts of it we filled in.
				mCombo.setSelection(new Point(mCombo.getText().length(), mOriginalText.length()));
			}
		}

		@Override
		public void keyPressed(KeyEvent pE) {
			switch (pE.keyCode) {
			case SWT.BS:  // If the used pressed backspace, this triggers a special reaction above, so we need to record it.
				mKeyUsed = pE.keyCode;
				break;
			case SWT.DEL: // If the user pressed delete and deleted something, we don't want to autofill again immediately.
				if (mCombo.getSelection().x != mCombo.getSelection().y)
					mSkipNext = true;
				break;
			case 'a':
				if (pE.stateMask == SWT.CTRL)
					// This stops the system deselecting everything after the user has pressed ctrl-a
					mSkipNext = true;
				break;
			default:
				mKeyUsed = -1;
			}
		}

		@Override
		public void keyReleased(KeyEvent pE) {
			// Not interesting for us
		}
		
	}
	
	private static final Object EMPTY_ELEMENT = new Object(); // Sentinel
	private static final String DEFAULT_ELEMENT_LABEL = ""; // The empty String.
	
	// We keep this value nullable in case we need to add some list functionality in the future.  It will be needed for get(), set(), indexed add(), and so on.
	private Boolean mDefaultElementLocation;
	private Map<String, Object> mContents;
	private org.eclipse.swt.widgets.Combo mCombo;
}
